package cadtoolbox.optimizer;

import java.awt.event.MouseAdapter;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;



import cadtoolbox.model.OligoGraph;
import cadtoolbox.model.OligoSystem;

import cadtoolbox.model.SequenceVertex;
import cadtoolbox.optimizers.cmaes.CMAEvolutionStrategy;
import cadtoolbox.optimizers.cmaes.fitness.IObjectiveFunction;

import cadtoolbox.utils.PlotExpData;
import cadtoolbox.utils.ProgressWindow;
import cadtoolbox.utils.SimulationProgressChangeListener;
import cadtoolbox.utils.WorkerCancelMouseAdapter;

public class OptimizerCMAES extends Thread implements IObjectiveFunction {

	
	private ArrayList<Parameter> parameters;
	private ArrayList<ReferencePoint> targets;
	private OligoGraph<SequenceVertex,String> graph;
	private Random rand;
	private PlotExpData plot;
	public CMAEvolutionStrategy cma = new CMAEvolutionStrategy();
	private int maxTime=Short.MAX_VALUE;
	
	public OptimizerCMAES(ArrayList<Parameter> parameters, ArrayList<ReferencePoint> targets, OligoGraph<SequenceVertex, String> graph, PlotExpData plot){
		this.parameters = parameters;
		this.targets = targets;
		this.graph = graph;
		this.plot = plot;
		this.rand = new Random();
		cma.readProperties(); // read options, see file CMAEvolutionStrategy.properties
		cma.setDimension(parameters.size()); // overwrite some loaded properties
		double[] initialX = new double[parameters.size()];
		for(int i=0; i < parameters.size(); ++i){
			initialX[i] = Math.acos(1-2*(parameters.get(i).currentValue-parameters.get(i).minValue)/(parameters.get(i).maxValue-parameters.get(i).minValue))/Math.PI;
		}
		
		cma.setInitialX(initialX); // in each dimension, also setTypicalX can be used
		cma.setInitialStandardDeviation(Constants.sigma); // also a mandatory setting 
		cma.options.stopFitness = parameters.size()/Constants.thresholdRatio;       // optional setting
	}
	@Override
	public double valueOf(double[] x) {
		double dist = 0;
		double[][] timeSerie = generateTimeSerie(x);
		for(ReferencePoint ref: targets){
			dist += (timeSerie[ref.index][ref.time]-ref.target)*(timeSerie[ref.index][ref.time]-ref.target);
		}
		return dist;
	}
	
	private double[][] generateTimeSerie(double[] x) {
		//OligoGraph<SequenceVertex,String> copy = (OligoGraph<SequenceVertex,String>) graph.clone();
		for(int i = 0; i < parameters.size(); i++){
			double transformedx = 0.5 - Math.cos(x[i]*Math.PI)/2.0;
			parameters.get(i).externalModification(transformedx*(parameters.get(i).maxValue-parameters.get(i).minValue)+parameters.get(i).minValue, graph);
		}
		OligoSystem<String> syst = new OligoSystem<String>(graph);
		return syst.calculateTimeSeries(null);
	}
	@Override
	public boolean isFeasible(double[] x) {
		return true;
	}

	private void setGraph(double[] x){
		for(int i = 0; i < parameters.size(); i++){
			double transformedx = 0.5 - Math.cos(x[i]*Math.PI)/2.0;
			parameters.get(i).externalModification(transformedx*(parameters.get(i).maxValue-parameters.get(i).minValue)+parameters.get(i).minValue, graph);
		}
	}
	
	public void run(){	
		
		boolean wasAutoplot = PlotExpData.autoplot;
		PlotExpData.autoplot = false;
		ProgressWindow pw = new ProgressWindow(this);
		pw.setVisible(true);
		// initialize cma and get fitness array to fill in later
		
		double[] fitness = cma.init();  // new double[cma.parameters.getPopulationSize()];

		// initial output to files
		cma.writeToDefaultFilesHeaders(0); // 0 == overwrites old files

		// iteration loop
		
		while(cma.stopConditions.getNumber() == 0) {
			
            // --- core iteration step ---
			double[][] pop = cma.samplePopulation(); // get a new population of solutions
//			for(int i = 0; i < pop.length; ++i) {    // for each candidate solution i
//            	// a simple way to handle constraints that define a convex feasible domain  
//            	// (like box constraints, i.e. variable boundaries) via "blind re-sampling" 
//            	                                       // assumes that the feasible domain is convex, the optimum is  
//				while (!this.isFeasible(pop[i]))     //   not located on (or very close to) the domain boundary,  
//					pop[i] = cma.resampleSingle(i);    //   initialX is feasible and initialStandardDeviations are  
//                                                       //   sufficiently small to prevent quasi-infinite looping here
//                // compute fitness/objective value	
//				fitness[i] = this.valueOf(pop[i]); // fitfun.valueOf() is to be minimized
//			}
			
			fitness = evaluateFitness(pop,pw);
			
			//PlotExpData.autoplot = true;
			//this.generateTimeSerie(cma.getBestX());
			this.setGraph(cma.getBestX());
			PlotExpData.autoplot = true;
			graph.replot();
			PlotExpData.autoplot = false;
			cma.updateDistribution(fitness);         // pass fitness array to update search distribution
            // --- end core iteration step ---

			// output to files and console 
			cma.writeToDefaultFiles();
			int outmod = 150;
			if (cma.getCountIter() % (15*outmod) == 1){
				String output = cma.getPrintAnnotation(); // might write file as well
				System.out.println(output);
				pw.jTextArea.append(output+"\n");
			}
			if (cma.getCountIter() % outmod == 1){
				String output = cma.getPrintLine();
				System.out.println(output);
				pw.jTextArea.append(output+"\n");
			}
				
				//cma.println(); 
		}
		// evaluate mean value as it is the best estimator for the optimum
		cma.setFitnessOfMeanX(this.valueOf(cma.getMeanX())); // updates the best ever solution 

		// final output
		cma.writeToDefaultFiles(1);
		//cma.println();
		String output = cma.getPrintLine();
		System.out.println(output);
		pw.jTextArea.append(output+"\n");
		output = "Terminated due to\n";
		
		//cma.println("Terminated due to");
		for (String s : cma.stopConditions.getMessages())
			output += "  " + s + "\n";	//cma.println("  " + s);
		output += "best function value " + cma.getBestFunctionValue() 
		+ " at evaluation " + cma.getBestEvaluationNumber(); //cma.println("best function value " + cma.getBestFunctionValue() 
				//+ " at evaluation " + cma.getBestEvaluationNumber());
		System.out.println(output);
		pw.jTextArea.append(output+"\n");
		this.setGraph(cma.getBestX());
			
		// we might return cma.getBestSolution() or cma.getBestX()
		PlotExpData.autoplot = true;
		graph.replot();
		PlotExpData.autoplot = wasAutoplot;

	}
	
	public void stopNow(){
		if(cma.stopConditions.getNumber()==0){
		JOptionPane.showMessageDialog(null, "Stopping");
		cma.options.stopnow = true;
		}
	}
	
	public double[] evaluateFitness(double[][] pop, ProgressWindow pw) {
		ExecutorService eservice;
		CompletionService<Object> cservice;
		pw.jPanelProgress.removeAll();
		pw.jPanelProgress.add(new JLabel("Iteration "+cma.getCountIter()));
		eservice = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors()/2,1));
		cservice = new ExecutorCompletionService<Object>(eservice);
		Double[] results = new Double[pop.length];
		int taskCount = 0;
		ArrayList<Future<Object>> futures = new ArrayList<Future<Object>>();
		for (int i = 0; i < pop.length; i++) {
			while (!this.isFeasible(pop[i]))     //   not located on (or very close to) the domain boundary,  
				pop[i] = cma.resampleSingle(i); 
			FitnessEvaluation eval = new FitnessEvaluation(i,pop[i],targets,parameters,graph);
			Future fut = cservice.submit(eval);
			JProgressBar bar = new JProgressBar(0,100);
			bar.setValue(0);
			bar.addMouseListener(new WorkerCancelMouseAdapter(fut));
			eval.addPropertyChangeListener(new SimulationProgressChangeListener(bar));
			pw.jPanelProgress.add(bar);
			pw.jPanelProgress.revalidate();
			pw.jPanelProgress.repaint();
			
			futures.add(fut);
			taskCount++;
		}

		for (int i = 0; i < taskCount; i++) {
			try {
				// System.out.println(this.maxRunTime);
				long t0 = System.currentTimeMillis();
				Future<Object> future = cservice.poll(3*maxTime, TimeUnit.SECONDS);
				long t1 = System.currentTimeMillis();
				if(maxTime == Short.MAX_VALUE){
					maxTime = (int) (t1-t0);
				}
				if (future != null) {
					FitnessEvaluation result = (FitnessEvaluation) future
							.get();
					int index = result.position;
					results[index] = result.fitness;
					//System.out.println(i + ":" + result.position + " done");
				} else {
					for (Future<Object> future1 : futures) {
						if (future1 != null) {
							future1.cancel(true);
						}
					}
					break;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		double[] ret = new double[results.length];
		for (int i = 0; i < results.length; i++) {
			if (results[i] == null) {
				//System.out.println(oligoSystems[i].toString());
				ret[i] = 0.0;
			} else {
				ret[i] = results[i];
			}
		}
		pw.jPanelProgress.removeAll();
		pw.jPanelProgress.add(new JLabel("Iteration "+cma.getCountIter()+" done"));
		pw.jPanelProgress.revalidate();
		pw.jPanelProgress.repaint();
		return ret;
	}
	
	public static class ReferencePoint{
		public int index; // which sequence we are talking about
		public int time; // from the discretized time
		public double target;
		
		public ReferencePoint(int index, int time, double target){
			this.index = index;
			this.target = target;
			this.time = time;
		}
	}
	
}
